const express = require("express")
const jwt = require("jsonwebtoken")
const crypto = require("crypto")
const { PrismaClient } = require("@prisma/client")
const { jwtBlackList, formatDateTime } = require("../utils/consts")



const p = new PrismaClient()


const router = express.Router()


router.post("/login", async (req, res) => {
    const { username = null, password = "" } = req.body
    const errors = {}
    if (username === null) errors.username = "is required"
    else if (username === "") errors.username = "cannot be empty"
    if (!password) errors.password = "is required"
    if (Object.keys(errors).length) {
        return res.status(422).json({
            msg: "data cannot be processed",
            errors
        })
    }
    const user = await p.users.findUnique({ where: { username } })


    if (user && user.password_hash === crypto.createHash("sha256").update(password).digest("base64")) {
        const token = jwt.sign({
            exp: Math.floor(Date.now() / 1000) + (5 * 60), // 5 min
            data: { id: user.id, username: user.username, is_active: user.is_active, is_admin: user.is_admin }
        }, process.env.SECRET)

        return res.json({
            msg: "success",
            token
        })
    }
    return res.status(401).json({
        msg: "Invalid username or password."
    })
})

router.get("/logout", (req, res) => {
    const tkn = req.header("Authorization")?.split(" ")[1]
    if (tkn) {
        jwtBlackList.add(tkn)
    }
    res.json({ msg: "success" })
})


router.post("/register", async (req, res) => {
    const { username = null, password = "", confirm_password = "" } = req.body
    errors = {}
    if (username === null) errors.username = "is required"
    if (username === "") errors.username = "cannot be empty"
    if (!password) errors.password = "is required"
    if (!confirm_password) errors.confirm_password = "is required"
    if (password !== confirm_password) errors.confirm_password = "not the same as the password"
    if (Object.keys(errors).length) {
        return res.status(422).json({
            msg: "data cannot be processed",
            errors
        })
    }
    const password_hash = crypto.createHash("sha256").update(password).digest("base64")
    try {
        const user = await p.users.create({
            data: { username, password_hash }
        })
        return res.status(201).json({
            msg: "created"
        })
    }
    catch (error) {
        if (error.code === "P2002") {
            return res.status(422).json({ msg: "data cannot be processed", errors: { username: "already exists" } })
        }
        return res.status(500).json({ error })
    }

})

router.get("/books", async (req, res) => {
    const { search = "", category_id = null, tag_id = null, sort = "id", order = "asc", page = 1, per_page = 6 } = req.query
    const where = {}
    if (search) {
        where.OR = [
            { title: { contains: search } },
            { author: { contains: search } },
            {
                book_tags: {
                    some: {
                        tag: {
                            name: { contains: search }
                        }
                    }
                }
            }
        ]
    }
    if (category_id) {
        where.category_id = +category_id
    }
    if (tag_id) {
        where.book_tags = { some: { tag: { id: +tag_id } } }
    }
    const books = await p.books.findMany({
        where,
        include: {
            category: {
                select: { id: true, name: true }
            },
            book_tags: {
                select: { tag: true }
            }
        },
        orderBy: { [sort]: order },
        take: +per_page,
        skip: (page - 1) * per_page
    })
    const _books = books.map(book => {
        const tags = book.book_tags.map(book_tag => {
            return book_tag.tag.name
        })
        delete book.category_id
        delete book.book_tags
        return {
            ...book,
            created_at: formatDateTime(book.created_at),
            updated_at: formatDateTime(book.updated_at),
            avg_rating: book.avg_rating == null ? null : +book.avg_rating,
            tags
        }
    })
    const total = await p.books.count({
        where
    })
    return res.json({
        msg: "success",
        data: _books,
        page: +page,
        per_page: +per_page,
        total: +total
    })
})

router.get("/books/recommendations", async (req, res) => {
    const _rows = await p.$queryRaw`call GetUserRecommendations(${2},${5})`
    const rows = _rows.map(r => ({
        id: r.f0,
        title: r.f1,
        author: r.f2,
        year: r.f3,
        description: r.f4,
        category: {
            id: +r.f5,
            name: r.f6
        },
        tags: r.f7.split(", ").map(t => {
            [id, name] = t.split("_")
            return { id: +id, name }
        }),
        stock: r.f8,
        avg_rating: +r.f9,
        created_at: formatDateTime(r.f10),
        updated_at: formatDateTime(r.f11),
        score: +r.f12
    }))

    return res.json({
        msg: "success",
        data: rows
    })
})

router.get("/books/:id", async (req, res) => {
    const id = +req.params.id
    try {
        const book = await p.books.findUniqueOrThrow({
            where: { id },
            include: {
                category: {
                    select: { id: true, name: true }
                },
                book_tags: {
                    select: { tag: true }
                }
            },
        })
        const tags = book.book_tags.map(book_tag => {
            return book_tag.tag.name
        })
        delete book.category_id
        delete book.book_tags
        const _book = {
            ...book,
            created_at: formatDateTime(book.created_at),
            updated_at: formatDateTime(book.updated_at),
            avg_rating: book.avg_rating == null ? null : +book.avg_rating,
            tags
        }
        return res.status(200).json({
            msg: "success",
            data: _book
        })
    }
    catch (error) {
        if (error.code === "P2025") {
            return res.status(404).json({ msg: "the book is not found" })
        }
        return res.status(500).json(error)
    }
})

router.post("/books", async (req, res) => {
    if (!req.auth.data.is_admin) {
        return res.status(403).json({
            msg: "forbidden, admin is required"
        })
    }

    const { title = null, author = null, year = null, description = null, category_id = null, tags = null, stock = null } = req.body
    const errors = {}
    if (title === null) errors.title = "is required"
    else if (typeof title !== "string") errors.title = "must be a string"
    else if (title?.trim() === "") errors.title = "cannot be empty"
    if (author === null) errors.author = "is required"
    else if (typeof author !== "string") errors.author = "must be a string"
    else if (author?.trim() === "") errors.author = "cannot be empty"
    if (year === null) errors.year = "is required"
    else if (!Number.isInteger(year)) errors.year = "must be an integer"
    else if (year <= 0) errors.year = "should be greater than 0"
    if (description === null) errors.description = "is required"
    else if (typeof description !== "string") errors.description = "must be a string"
    else if (description?.trim() === "") errors.description = "cannot be empty"
    if (category_id === null) errors.category_id = "is required"
    else if (!Number.isInteger(category_id)) errors.category_id = "must be an integer"
    else if (category_id <= 0) errors.category_id = "should be greater than 0"
    if ((await p.categories.count({ where: { id: category_id } })) === 0) errors.category_id = "should exist in the database"
    if (tags !== null) {
        if (Array.isArray(tags)) {
            let all_string = true
            tags.forEach(tag => {
                if (typeof tag !== "string") {
                    all_string = false
                }
            })
            if (!all_string) errors.tags = "should be a string array or an empty array"
        }
        else {
            errors.tags = "should be a string array or an empty array"
        }
    }
    if (stock === null) errors.stock = "is required"
    else if (!Number.isInteger(stock)) errors.stock = "must be an integer"
    else if (stock <= 0) errors.stock = "should be greater than 0"

    if (Object.keys(errors).length) {
        return res.status(422).json({
            msg: "data cannot be processed",
            errors
        })
    }
    delete req.body.tags
    delete req.body.category_id
    const book = await p.books.create({
        data: {
            ...req.body,
            category: {
                connect: {
                    id: category_id
                }
            },
            book_tags: {
                create: tags.map(tag => ({
                    tag: {
                        connectOrCreate: {
                            where: { name: tag },
                            create: { name: tag }
                        }
                    }
                }))
            }
        },

    })

    return res.status(201).json({
        msg: "created",
        data: { id: book.id }
    })
})

router.get("/categories", async (req, res) => {
    return res.status(200).json({
        msg: "success",
        data: await p.categories.findMany()
    })
})

router.get("/tags", async (req, res) => {
    return res.status(200).json({
        msg: "success",
        data: await p.tags.findMany()
    })
})

router.get("/books/:id/borrow", async (req, res) => {
    const book_id = req.params.id
    const _book = await p.books.findUnique({ where: { id: +book_id } })
    if (!_book) {
        return res.status(404).json({
            msg: "the book is not found"
        })
    }
    if (_book.stock < 1) {
        return res.status(422).json({
            msg: "the book is out of stock",
            data: {
                id: _book.id,
                title: _book.title,
                stock: _book.stock
            }
        })
    }
    try {
        let rec, book;
        await p.$transaction(async (ctx) => {
            rec = await ctx.borrow_records.create({
                data: {
                    user: {
                        connect: { id: req.auth.data.id }
                    },
                    book: {
                        connect: { id: +book_id }
                    }
                }
            })
            if (rec) {
                book = await ctx.books.update({
                    where: { id: +book_id },
                    data: {
                        stock: { decrement: 1 }
                    }
                })
            }
        })
        req.app.get("io").emit("book_borrowed", +book_id)
        delete rec.book_id
        return res.status(200).json({
            msg: "success",
            data: {
                ...rec,
                borrow_date: formatDateTime(rec.borrow_date),
                book: {
                    id: +book_id,
                    title: book.title,
                    stock: book.stock
                }
            }
        })
    }
    catch (error) {
        return res.status(500).json(error)
    }

})

router.post("/books/borrow", async (req, res) => {
    const { book_ids = null } = req.body
    const errors = {}
    if (book_ids === null) errors.book_ids = "is required"
    else if (!Array.isArray(book_ids)) errors.book_ids = "must be an interger array"
    else if (book_ids.length === 0) errors.book_ids = "must be an interger array"
    else {
        let all_int = true
        book_ids.forEach(id => {
            if (!Number.isInteger(id)) all_int = false
        })
        if (!all_int) errors.book_ids = "must be an interger array"
    }

    if (Object.keys(errors).length) {
        return res.status(400).json({
            msg: "bad request",
            errors
        })
    }

    const result = await Promise.all(book_ids.map(async id => {
        const book = await p.books.findUnique({ where: { id } })
        if (book) {
            return { id: book.id, title: book.title, stock: book.stock, error: book.stock > 0 ? "" : "out of stock" }
        }
        return { id, title: null, stock: null, error: "not found" }
    }))
    //console.log(result);
    const r_errors = result.filter(r => r.error.length > 0)
    if (r_errors.length) {
        return res.status(422).json({
            msg: "data cannot be processed",
            errors: r_errors
        })
    }

    try {
        let all_borrowed_book_recs, records
        await p.$transaction(async ctx => {
            all_borrowed_book_recs = await Promise.all(book_ids.map(async book_id => {
                const rec = await ctx.borrow_records.create({
                    data: {
                        user: {
                            connect: { id: req.auth.data.id }
                        },
                        book: { connect: { id: book_id } }
                    }
                })
                const _book = await ctx.books.update({
                    where: { id: book_id },
                    data: {
                        stock: { decrement: 1 }
                    }
                })
                req.app.get("io").emit("book_borrowed", _book.id)
                return [rec, _book]
            }))

            records = all_borrowed_book_recs.map(([rec, book]) => {
                delete rec.book_id
                return {
                    ...rec,
                    borrow_date: formatDateTime(rec.borrow_date),
                    book: {
                        id: book.id,
                        title: book.title,
                        stock: book.stock
                    }
                }
            })

        })
        return res.status(200).json({
            msg: "success",
            records
        })

    }
    catch (err) {
        return res.status(500).json(err.toString())
    }


})

router.post("/books/:id/comments", async (req, res) => {
    const book_id = req.params.id
    if (!(await p.books.count({ where: { id: +book_id } }))) {
        return res.status(404).json({
            msg: "the book is not found"
        })
    }
    const { rating = null, comment = null } = req.body
    const errors = {}
    if (rating === null) errors.rating = "is required"
    else if (!Number.isInteger(rating)) errors.rating = "must be an integer"
    else if (rating < 0 || rating > 5) errors.rating = "should be greater than 0 and less than 6"
    if (comment === null) errors.comment = "is required"
    else if (typeof comment !== "string") errors.comment = "must be a string"
    else if (comment?.trim() === "") errors.comment = "cannot be empty"
    if (Object.keys(errors).length) {
        return res.status(422).json({
            msg: "data cannot be processed",
            errors
        })
    }

    try {
        let _comment, book
        await p.$transaction(async ctx => {
            _comment = await ctx.comments.create({
                data: {
                    comment,
                    rating,
                    user: {
                        connect: { id: req.auth.data.id }
                    },
                    book: { connect: { id: +book_id } }
                }
            })
            const aggr = await ctx.comments.aggregate({
                _avg: { rating: true },
                where: { book: { id: +book_id } }
            })
            //console.log(aggr._avg.rating);
            book = await ctx.books.update({
                where: { id: +book_id },
                data: { avg_rating: +aggr._avg.rating }
            })

        })
        return res.status(201).json({
            msg: "created",
            data: {
                user_id: _comment.user_id,
                rating: _comment.rating,
                comment: _comment.comment,
                created_at: formatDateTime(_comment.created_at)
            }
        })

    }
    catch (err) {
        return res.status(500).json(err.toString())
    }

})

router.get("/admin/stats", async (req, res) => {
    if (!req.auth.data.is_admin) return res.status(403).json({ msg: "forbidden, admin is required" })
    const total_borrows = await p.borrow_records.count({})
    const active_borrows = await p.borrow_records.count({ where: { return_date: null } })
    const topUsers = await p.borrow_records.groupBy({
        by: ['user_id'],
        _count: {
            id: true, // Count the number of borrow records
        },
        orderBy: {
            _count: {
                id: 'desc',
            },
        },
        take: 5, // Get top 5 users
    });
    const topBooks = await p.borrow_records.groupBy({
        by: ['book_id'],
        _count: {
            id: true, // Count the number of borrow records
        },
        orderBy: {
            _count: {
                id: 'desc',
            },
        },
        take: 5, // Get top 5 books
    });
    // topBooks:
    // [
    //     {
    //         "_count": {
    //         "id": 2
    //         },
    //         "book_id": 2
    //     },
    //     ...
    // ]
    const top_books = await Promise.all(topBooks.map(async b => {
        const { _count, book_id } = b
        const book = await p.books.findUnique({ where: { id: book_id }, select: { id: true, title: true } })
        return { book_id: book.id, title: book.title, borrow_count: _count.id }
    }))

    const top_users = await Promise.all(topUsers.map(async u => {
        const { _count, user_id } = u
        const user = await p.users.findUnique({ where: { id: user_id }, select: { id: true, username: true } })
        return { user_id: user.id, username: user.username, borrow_count: _count.id }
    }))

    return res.json({
        total_borrows,
        active_borrows,
        top_users,
        top_books
    })
})

router.post("/admin/users/:id/toggle", async (req, res) => {
    if (!req.auth.data.is_admin) return res.status(403).json({ msg: "forbidden, admin is required" })
    const user = await p.users.findUnique({ where: { id: +req.params.id } })
    if (!user) return res.status(404).json({ msg: "the user is not found" })
    try {
        const is_active = user.is_active
        const _user = await p.users.update({ where: { id: user.id }, data: { is_active: !is_active } })
        return res.status(200).json({
            msg: "success",
            data: {
                user_id: _user.id,
                is_active: _user.is_active
            }
        })
    }
    catch (err) {
        return res.status(500).json(err)
    }
})





module.exports = router